{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#default_exp radar.v1\n",
    "from nbdev.showdoc import show_doc"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# radar.v1\n",
    "\n",
    "RadarFrame encapsulates the signal processing for getting different stages of the standard FMCW radar processing pipeline.\n",
    "\n",
    "More details can be found at [V1RadarExamples]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#hide\n",
    "\n",
    "#For an easier life when developing\n",
    "%load_ext autoreload\n",
    "%autoreload 2"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "\n",
    "import numpy as np\n",
    "from mmwave import dsp"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#| export\n",
    "\n",
    "class RadarFrame(object):\n",
    "    \"\"\"Encapsulates processing of a radar frame data cube.\n",
    "    The RadarFrame object can be initialized once, so long as there's no change in\n",
    "    the radar configuration.\n",
    "    When new radar data is available, set raw radar data using the `raw_cube` property.\n",
    "\n",
    "    Various processing of the radar data are computed lazily on the first\n",
    "    access to each property. Processing can also be triggered manually by calling\n",
    "    the related methods.\n",
    "\n",
    "    This object stores intemediate steps to avoid recomputation when possible.\n",
    "    If no new raw datacube is passed in, subsequent request for different views of\n",
    "    the radar data are returned from the stored state.\n",
    "    \"\"\"\n",
    "    def __init__(self, radar_config,\n",
    "                 angle_res = 1,\n",
    "                 angle_range = 90,\n",
    "                 origin_at_bottom_center = True,\n",
    "                 use_float32 = False,\n",
    "                ):\n",
    "        super(RadarFrame, self).__init__()\n",
    "        self.cfg = radar_config\n",
    "\n",
    "        #Beamforming params\n",
    "        self.bins_processed = self.cfg['profiles'][0]['adcSamples'] #radar_cube.shape[0]\n",
    "        self.virt_ant = self.cfg['numLanes'] * len(self.cfg['chirps']) #radar_cube.shape[1]\n",
    "        self.__doppler_bins = self.cfg['numChirps'] // len(self.cfg['chirps']) #radar_cube.shape[2]\n",
    "        self.angle_res = angle_res\n",
    "        self.angle_range = angle_range\n",
    "        self.angle_bins = (self.angle_range * 2) // self.angle_res + 1\n",
    "        self.num_vec, self.steering_vec = dsp.gen_steering_vec(self.angle_range,\n",
    "                                                               self.angle_res,\n",
    "                                                               self.virt_ant)\n",
    "\n",
    "        #Properties\n",
    "        self.__range_azimuth_dirty = True\n",
    "        self.__range_azimuth = np.zeros((self.bins_processed, self.angle_bins),\n",
    "                                        dtype=np.complex64 if use_float32 else np.complex_)\n",
    "        self.__beam_weights = np.zeros((self.virt_ant, self.bins_processed),\n",
    "                                       dtype=np.complex64 if use_float32 else np.complex_)\n",
    "        self.__range_doppler = None\n",
    "        self.__raw_cube = None\n",
    "        self.__range_cube = None\n",
    "\n",
    "        self.__flip_ra = origin_at_bottom_center\n",
    "\n",
    "    @property\n",
    "    def flipped(self):\n",
    "        \"\"\"True if zero range is at bottom center of image,\n",
    "        i.e. y is flipped from typical image pixel coordinates with 0,0 at top left corner.\n",
    "        This is important for computing the intrinsic matrix\"\"\"\n",
    "        return self.__flip_ra\n",
    "\n",
    "    @property\n",
    "    def range_nbins(self):\n",
    "        \"\"\"Number of bins in range\"\"\"\n",
    "        return self.bins_processed\n",
    "\n",
    "    @property\n",
    "    def max_range(self):\n",
    "        \"\"\"Maximum range in meters, this property is computed from the radar configuration\"\"\"\n",
    "        return self.range_resolution * self.range_nbins\n",
    "\n",
    "    @property\n",
    "    def range_resolution(self):\n",
    "        \"\"\"Range resolution in meters\"\"\"\n",
    "        range_res, bw = dsp.range_resolution(self.cfg['profiles'][0]['adcSamples'],\n",
    "                                             self.cfg['profiles'][0]['adcSampleRate'] / 1000,\n",
    "                                             self.cfg['profiles'][0]['freqSlopeConst'] / 1e12)\n",
    "        return range_res\n",
    "    \n",
    "    @property\n",
    "    def doppler_bins(self):\n",
    "        \"\"\"Number of bins in doppler dimension\"\"\"\n",
    "        return self.__doppler_bins\n",
    "\n",
    "    @property\n",
    "    def doppler_resolution(self):\n",
    "        \"\"\"Doppler resolution in m/s\"\"\"\n",
    "        _, bw = dsp.range_resolution(self.cfg['profiles'][0]['adcSamples'],\n",
    "                                             self.cfg['profiles'][0]['adcSampleRate'] / 1000,\n",
    "                                             self.cfg['profiles'][0]['freqSlopeConst'] / 1e12)\n",
    "        return dsp.doppler_resolution(bw,\n",
    "                                      start_freq_const=self.cfg['profiles'][0]['start_frequency'] / 1e9,\n",
    "                                      ramp_end_time=self.cfg['profiles'][0]['rampEndTime'] * 1e6,\n",
    "                                      idle_time_const=self.cfg['profiles'][0]['idle'] * 1e6,\n",
    "                                      num_loops_per_frame=self.cfg['numChirps'] / len(self.cfg['chirps']),\n",
    "                                      num_tx_antennas=self.cfg['numTx'])\n",
    "\n",
    "\n",
    "    @property\n",
    "    def max_unambiguous_doppler(self):\n",
    "        \"\"\"Maximum unambiguous doppler in m/s.\n",
    "        In FMCW radar processing, aliasing will occur when the object's relative Doppler\n",
    "        is above this value.\n",
    "        i.e. if `max_unambiguous_doppler` is 10m/s, and the object is at 11 m/s, the computed doppler will be -9 m/s.\n",
    "        \"\"\"\n",
    "        return self.doppler_resolution * self.doppler_bins / 2\n",
    "        raise NotImplementedError\n",
    "\n",
    "    @property\n",
    "    def raw_cube(self):\n",
    "        \"\"\"Raw radar data cube\n",
    "        Set the radar data cube using this property.\n",
    "        `rf.raw_cube = radar_cube`\n",
    "\n",
    "        This will clear all cached processed data like `range_cube`, `range_azimuth`, etc.\n",
    "        \"\"\"\n",
    "        return self.__raw_cube\n",
    "\n",
    "    @raw_cube.setter\n",
    "    def raw_cube(self, raw_cube):\n",
    "        self.__raw_cube = raw_cube\n",
    "        self.__range_cube = None\n",
    "        self.__range_doppler = None\n",
    "        self.__range_azimuth_dirty = True\n",
    "\n",
    "    @property\n",
    "    def range_cube(self):\n",
    "        \"\"\"Get the radar data as a range cube.\n",
    "        Processing from the raw cube is done lazily on the first access to this property.\n",
    "        \"\"\"\n",
    "        if self.__range_cube is not None:\n",
    "            return self.__range_cube\n",
    "        else:\n",
    "            range_cube = dsp.range_processing(self.raw_cube)\n",
    "            self.__range_cube = np.swapaxes(range_cube, 0, 2)\n",
    "            return self.__range_cube\n",
    "\n",
    "    @property\n",
    "    def range_doppler(self):\n",
    "        \"\"\"Get the radar data as a range doppler cube.\n",
    "        Processing from the raw cube is done lazily on the first access to this property.\n",
    "        \"\"\"\n",
    "        if self.__range_doppler is not None:\n",
    "            return self.__range_doppler\n",
    "        else:\n",
    "            range_doppler = dsp.doppler_processing(self.raw_cube)\n",
    "            self.__range_doppler = range_doppler\n",
    "            return self.__range_doppler\n",
    "\n",
    "    @property\n",
    "    def range_azimuth_capon(self):\n",
    "        \"\"\"Get the radar data as a range azimuth cube,\n",
    "        beamformed using the Capon beamformer.\n",
    "        This property is computed lazily on first access.\n",
    "        \"\"\"\n",
    "        if not self.__range_azimuth_dirty:\n",
    "            r = self.__range_azimuth\n",
    "        else:\n",
    "            self.__aoa_capon_process()\n",
    "            r = self.__range_azimuth\n",
    "\n",
    "        if self.__flip_ra:\n",
    "            return np.flipud(np.fliplr(r))\n",
    "        else:\n",
    "            return r\n",
    "\n",
    "    def __aoa_capon_process(self):\n",
    "        radar_cube = self.range_cube\n",
    "\n",
    "        for jj in range(self.bins_processed):\n",
    "            self.__range_azimuth[jj,:], self.__beam_weights[:,jj] = dsp.aoa_capon(radar_cube[jj],\n",
    "                                                                      self.steering_vec)\n",
    "\n",
    "        self.__range_azimuth_dirty = False\n",
    "\n",
    "    def compute_range_azimuth(self, radar_raw=None, method='capon'):\n",
    "        \"\"\"Beamform raw radar datacube\n",
    "        Use this method to set the the raw radar data cube and \n",
    "        perform beamforming. The beamformed data cube will be returned.\n",
    "        \n",
    "        Currently only the capon method is implemented.\n",
    "\n",
    "        This is a convenience method that is equivalent to:\n",
    "        ```\n",
    "        # rf = RadarFrame(...)\n",
    "        rf.raw_cube = radar_raw\n",
    "        beamformed_datacube = rf.range_azimuth_capon\n",
    "        ```\n",
    "        \"\"\"\n",
    "        if radar_raw is not None:\n",
    "            self.raw_cube = radar_raw\n",
    "\n",
    "        if method == 'capon':\n",
    "            return self.range_azimuth_capon\n",
    "        else:\n",
    "            raise NotImplementedError"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/markdown": [
       "<h4 id=\"RadarFrame.range_nbins\" class=\"doc_header\"><code>RadarFrame.range_nbins</code><a href=\"\" class=\"source_link\" style=\"float:right\">[source]</a></h4>\n",
       "\n",
       "Number of bins in range"
      ],
      "text/plain": [
       "<IPython.core.display.Markdown object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "text/markdown": [
       "<h4 id=\"RadarFrame.max_range\" class=\"doc_header\"><code>RadarFrame.max_range</code><a href=\"\" class=\"source_link\" style=\"float:right\">[source]</a></h4>\n",
       "\n",
       "Maximum range in meters, this property is computed from the radar configuration"
      ],
      "text/plain": [
       "<IPython.core.display.Markdown object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "text/markdown": [
       "<h4 id=\"RadarFrame.range_resolution\" class=\"doc_header\"><code>RadarFrame.range_resolution</code><a href=\"\" class=\"source_link\" style=\"float:right\">[source]</a></h4>\n",
       "\n",
       "Range resolution in meters"
      ],
      "text/plain": [
       "<IPython.core.display.Markdown object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "text/markdown": [
       "<h4 id=\"RadarFrame.doppler_resolution\" class=\"doc_header\"><code>RadarFrame.doppler_resolution</code><a href=\"\" class=\"source_link\" style=\"float:right\">[source]</a></h4>\n",
       "\n",
       "Doppler resolution in m/s"
      ],
      "text/plain": [
       "<IPython.core.display.Markdown object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "text/markdown": [
       "<h4 id=\"RadarFrame.max_unambiguous_doppler\" class=\"doc_header\"><code>RadarFrame.max_unambiguous_doppler</code><a href=\"\" class=\"source_link\" style=\"float:right\">[source]</a></h4>\n",
       "\n",
       "Maximum unambiguous doppler in m/s.\n",
       "In FMCW radar processing, aliasing will occur when the object's relative Doppler\n",
       "is above this value.\n",
       "i.e. if `max_unambiguous_doppler` is 10m/s, and the object is at 11 m/s, the computed doppler will be -9 m/s."
      ],
      "text/plain": [
       "<IPython.core.display.Markdown object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "text/markdown": [
       "<h4 id=\"RadarFrame.doppler_bins\" class=\"doc_header\"><code>RadarFrame.doppler_bins</code><a href=\"\" class=\"source_link\" style=\"float:right\">[source]</a></h4>\n",
       "\n",
       "Number of bins in doppler dimension"
      ],
      "text/plain": [
       "<IPython.core.display.Markdown object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "text/markdown": [
       "<h4 id=\"RadarFrame.raw_cube\" class=\"doc_header\"><code>RadarFrame.raw_cube</code><a href=\"\" class=\"source_link\" style=\"float:right\">[source]</a></h4>\n",
       "\n",
       "Raw radar data cube\n",
       "Set the radar data cube using this property.\n",
       "`rf.raw_cube = radar_cube`\n",
       "\n",
       "This will clear all cached processed data like `range_cube`, `range_azimuth`, etc."
      ],
      "text/plain": [
       "<IPython.core.display.Markdown object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "text/markdown": [
       "<h4 id=\"RadarFrame.range_cube\" class=\"doc_header\"><code>RadarFrame.range_cube</code><a href=\"\" class=\"source_link\" style=\"float:right\">[source]</a></h4>\n",
       "\n",
       "Get the radar data as a range cube.\n",
       "Processing from the raw cube is done lazily on the first access to this property."
      ],
      "text/plain": [
       "<IPython.core.display.Markdown object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "text/markdown": [
       "<h4 id=\"RadarFrame.range_doppler\" class=\"doc_header\"><code>RadarFrame.range_doppler</code><a href=\"\" class=\"source_link\" style=\"float:right\">[source]</a></h4>\n",
       "\n",
       "Get the radar data as a range doppler cube.\n",
       "Processing from the raw cube is done lazily on the first access to this property."
      ],
      "text/plain": [
       "<IPython.core.display.Markdown object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "text/markdown": [
       "<h4 id=\"RadarFrame.range_azimuth_capon\" class=\"doc_header\"><code>RadarFrame.range_azimuth_capon</code><a href=\"\" class=\"source_link\" style=\"float:right\">[source]</a></h4>\n",
       "\n",
       "Get the radar data as a range azimuth cube,\n",
       "beamformed using the Capon beamformer.\n",
       "This property is computed lazily on first access."
      ],
      "text/plain": [
       "<IPython.core.display.Markdown object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "text/markdown": [
       "<h4 id=\"RadarFrame.compute_range_azimuth\" class=\"doc_header\"><code>RadarFrame.compute_range_azimuth</code><a href=\"__main__.py#L170\" class=\"source_link\" style=\"float:right\">[source]</a></h4>\n",
       "\n",
       "> <code>RadarFrame.compute_range_azimuth</code>(**`radar_raw`**=*`None`*, **`method`**=*`'capon'`*)\n",
       "\n",
       "Beamform raw radar datacube\n",
       "Use this method to set the the raw radar data cube and \n",
       "perform beamforming. The beamformed data cube will be returned.\n",
       "\n",
       "Currently only the capon method is implemented.\n",
       "\n",
       "This is a convenience method that is equivalent to:\n",
       "```\n",
       "# rf = RadarFrame(...)\n",
       "rf.raw_cube = radar_raw\n",
       "beamformed_datacube = rf.range_azimuth_capon\n",
       "```"
      ],
      "text/plain": [
       "<IPython.core.display.Markdown object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "show_doc(RadarFrame.range_nbins)\n",
    "show_doc(RadarFrame.max_range)\n",
    "show_doc(RadarFrame.range_resolution)\n",
    "show_doc(RadarFrame.doppler_resolution)\n",
    "show_doc(RadarFrame.max_unambiguous_doppler)\n",
    "show_doc(RadarFrame.doppler_bins)\n",
    "show_doc(RadarFrame.raw_cube)\n",
    "show_doc(RadarFrame.range_cube)\n",
    "show_doc(RadarFrame.range_doppler)\n",
    "show_doc(RadarFrame.range_azimuth_capon)\n",
    "show_doc(RadarFrame.compute_range_azimuth)"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3.10.4 64-bit",
   "language": "python",
   "name": "python3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
